package model.simulationmodel.logicelements;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import model.LogicType;
import model.editormodel.Input;
import model.editormodel.LogicEditorElement;
import model.editormodel.Output;
import model.simulationmodel.LogicValue;
import model.simulationmodel.SimulationModel;

/**
 * <b>Klasse:</b> abstract <br>
 * <b>Implementiert:</b> <code>LogicInterface</code> <br>
 * <b>In Verbindung stehende Klassen:</b>
 * <code>LogicValue, LogicEditorElement, SimulationModel
 * </code><br>
 * <b>Funktion: </b>Repraesentation eines Bauteils zur Simulation <br>
 * <br>
 * Eine abstrakte Klasse von der die logischen Elemente erben. Hat Informationen
 * ueber Ein- und Ausgangsbelegungen sowie eine Referenz auf das zugehoerige
 * <code>LogicEditorElement</code> und <code>SimulationModel</code>.
 * 
 * @author Team Locsi
 * 
 */
public abstract class LogicSimulationElement implements LogicInterface {

	private SimulationModel sModel;
	protected LogicEditorElement logicEditorElement;
	protected Map<String, LogicValue> logicOutputValueMap;
	protected Map<String, LogicValue> logicInputValueMap;
	private long changesPerPulse;

	/**
	 * Dieser Konstruktor erzeugt ein neues <code>LogicSimulationElement</code>.
	 * 
	 * @param sModel
	 *            <code>SimulationModel</code> - Referenz auf das
	 *            <code>SimulationModel</code>
	 * @param logicEditorElement
	 *            <code>LogicEditorElement</code> - Referenz auf das zugehoerige
	 *            <code>LogicEditorElement</code>
	 */
	public LogicSimulationElement(SimulationModel sModel,
			LogicEditorElement logicEditorElement) {
		this.sModel = sModel;
		this.logicEditorElement = logicEditorElement;
		this.logicInputValueMap = new HashMap<String, LogicValue>();
		this.logicOutputValueMap = new HashMap<String, LogicValue>();

		// Input-Map von logicEditorElement holen
		Map<String, Input> inputs = this.getLogicEditorElement().getInputs();

		// durchlaufen und namen holen und alle Eingaenge auf UNDEFINED setzen
		for (Input currentInput : inputs.values()) {
			// falls es sich bei Bauteil um einen Taktgeber bzw. Schalter
			// handelt wird der Eingangswert bzw. der eingestellte Wert auf L0
			// gesetzt ansonsten auf UNDEFINED
			if ((logicEditorElement.getType() == LogicType.SWITCH)
					|| (logicEditorElement.getType() == LogicType.CLOCK)) {
				logicInputValueMap.put(currentInput.getName(), LogicValue.L0);
			} else {
				logicInputValueMap.put(currentInput.getName(),
						LogicValue.UNDEFINED);
			}
		}

		// Output-Map von logicEditorElement holen
		Map<String, Output> outputs = this.getLogicEditorElement().getOutputs();

		// durchlaufen und namen holen und alle auf UNDEFINED setzen
		for (Output currentOutput : outputs.values()) {
			logicOutputValueMap.put(currentOutput.getName(),
					LogicValue.UNDEFINED);
		}
	}

	/**
	 * Diese Methode gibt das in diesem Objekt gespeicherte
	 * <code>LogicEditorElement</code> zurueck.
	 * 
	 * @return <code>LogicEditorElement</code>
	 */
	public LogicEditorElement getLogicEditorElement() {
		return logicEditorElement;
	}
	
	/**
	 * Wird in jedem Bauteil nach einem Takt aufgerufen, bzw wenn ein
	 * Schalter umgelegt wurde
	 */
	public void resetChangesPerPulse() {
		this.changesPerPulse = 0;
	}

	/**
	 * Setzt den logischen Wert an einem bestimmten Ausgang eines Bauteils, und
	 * gibt den Wert durch die propagateValue()-Methode des SimulationModels an
	 * seine nachfolgenden Bauteile weiter. Dabei wird ebenfalls geprueft ob
	 * eine Oszillation vorliegt.
	 * 
	 * @param outputName
	 *            <code>String</code> - Name des Ausgangs des Bauteils
	 * @param value
	 *            <code>LogicValue</code> - logischer Wert der gesetzt wird
	 */
	public void setOutputValue(String outputName, LogicValue value) {
		changesPerPulse++;
		
		if (changesPerPulse > sModel.getSimulationElements().size()) {

			// bei Oszillation wird der Ausgangswert auf undefined gesetzt
			value = LogicValue.UNDEFINED;

			// entsprechende fehlermeldung soll in der infoarea ausgegeben
			// werden aus diesem Grund wird zunaechst das model benachrichtigt,
			// die die aktion an die view weiterleitet
			sModel.oszillationFound(this.getLogicEditorElement().getOutputs()
					.get(outputName).getMySuperElement());
		}

		// setzen des neuen logischen Wertes bzw. weiterverbreiten des Wertes
		this.logicOutputValueMap.put(outputName, value);
		List<Input> inputs = logicEditorElement.getOutputs().get(outputName)
				.getTargetInputs();
		if (inputs.size() > 0) {
			sModel.propagateValue(inputs, value);
		}
	}

	/**
	 * Setzt den logischen Wert an einem bestimmten Eingang eines Bauteils.
	 * 
	 * @param inputName
	 *            <code>String</code> - Name des Eingangs des Bauteils
	 * @param value
	 *            <code>LogicValue</code> - logischer Wert der gesetzt wird
	 * 
	 * @return <code>boolean</code> - <code>true</code>, wenn ein logischer Wert
	 *         gesetzt wurde; <code>false</code> sonst
	 * 
	 */
	public boolean setInputValue(String inputName, LogicValue value) {
		if ((inputName != null) && (value != null)) {

			// Prueft, ob der uebergebene Name des Eingangs existiert
			if (logicEditorElement.getInputs().get(inputName) != null) {
				this.logicInputValueMap.put(inputName, value);
				return true;
			} else {
				return false;
			}
		} else {
			return false;
		}
	}

	/**
	 * Gibt die <code>Map</code> der logischen Werte an den Ausgaengen des
	 * Bauteils zurueck.
	 * 
	 * @return <code>Map</code> <<code>String, LogicValue></code> -
	 *         <code>Map</code> der logischen Werte an den Ausgaengen
	 */
	public Map<String, LogicValue> getLogicValues() {
		return logicOutputValueMap;
	}

	/**
	 * Gibt den logischen Wert an dem angeforderten Ausgang des Bauteils zurueck
	 * 
	 * @param name
	 *            <code>String</code> Name des Ausgangs
	 * @return <code>LogicValue</code> - logischer Wert des Ausgangs
	 */
	public LogicValue getLogicValue(String name) {
		return logicOutputValueMap.get(name);
	}

	/**
	 * Gibt den logischen Wert an dem angeforderten Eingang des Bauteils zurueck
	 * 
	 * @param name
	 *            <code>String</code> Name des Eingangs
	 * @return <code>LogicValue</code> - logischer Wert des Eingangs
	 */
	public LogicValue getLogicValueOfInput(String name) {
		return logicInputValueMap.get(name);
	}

	/**
	 * Setzt die Ein- und Ausgaenge des Bauteils auf UNDEFINED. Bei Schalter und
	 * Taktgeber werden nur die Ausgange auf UNDEFINED gesetzt die eingestellten
	 * Werte bleiben
	 */
	public void refactor() {
		if (!(this instanceof LogicCLOCK) && !(this instanceof LogicSwitch)) {
			// alle Eingaenge ausser bei Schalter und Taktgeber auf Undefined
			// setzen
			Set<String> inpSet = this.getLogicEditorElement().getInputs()
					.keySet();
			logicInputValueMap.remove(inpSet);

			for (String inp : inpSet) {
				logicInputValueMap.put(inp, LogicValue.UNDEFINED);
			}
		} else if (this instanceof LogicCLOCK) {
			logicInputValueMap.put(Input.STD_A,
					((LogicCLOCK) this).getStartValue());
		}

		Set<String> outSet = this.getLogicEditorElement().getOutputs().keySet();
		logicOutputValueMap.remove(outSet);

		for (String out : outSet) {
			logicOutputValueMap.put(out, LogicValue.UNDEFINED);
		}
		
		resetChangesPerPulse();	
	}
}